using System;
using System.Diagnostics;

using Org.BouncyCastle.Crypto.Parameters;
using SymmetricEncription.Presenters;

namespace Org.BouncyCastle.Crypto
{
    /**
    * A wrapper class that allows block ciphers to be used to process data in
    * a piecemeal fashion. The BufferedBlockCipher outputs a block only when the
    * buffer is full and more data is being added, or on a doFinal.
    * <p>
    * Note: in the case where the underlying cipher is either a CFB cipher or an
    * OFB one the last block may not be a multiple of the block size.
    * </p>
    */
    public class BufferedBlockCipher
    {
        internal byte[] buf;
        internal int bufOff;
        internal bool forEncryption;
        internal IBlockCipher cipher;

        /**
        * constructor for subclasses
        */
        protected BufferedBlockCipher()
        {
        }

        /**
        * Create a buffered block cipher without padding.
        *
        * @param cipher the underlying block cipher this buffering object wraps.
        * false otherwise.
        */
        public BufferedBlockCipher(
            IBlockCipher cipher)
        {
            if (cipher == null)
                throw new ArgumentNullException("cipher");

            this.cipher = cipher;
            buf = new byte[cipher.GetBlockSize()];
            bufOff = 0;
        }

        public string AlgorithmName
        {
            get { return cipher.AlgorithmName; }
        }

        /**
        * initialise the cipher.
        *
        * @param forEncryption if true the cipher is initialised for
        *  encryption, if false for decryption.
        * @param param the key and other data required by the cipher.
        * @exception ArgumentException if the parameters argument is
        * inappropriate.
        */
        // Note: This doubles as the Init in the event that this cipher is being used as an IWrapper
        public void Init(
            bool forEncryption,
            ICipherParameters parameters,
            ProcessStepBase previousStep, out ProcessStepBase currentStep)
        {
            this.forEncryption = forEncryption;

            Reset();

            cipher.Init(forEncryption, parameters, previousStep, out currentStep);
        }

        /**
        * return the blocksize for the underlying cipher.
        *
        * @return the blocksize for the underlying cipher.
        */
        public int GetBlockSize()
        {
            return cipher.GetBlockSize();
        }

        /**
        * return the size of the output buffer required for an update
        * an input of len bytes.
        *
        * @param len the length of the input.
        * @return the space required to accommodate a call to update
        * with len bytes of input.
        */
        public int GetUpdateOutputSize(
            int length)
        {
            int total = length + bufOff;
            int leftOver = total % buf.Length;
            return total - leftOver;
        }

        /**
        * return the size of the output buffer required for an update plus a
        * doFinal with an input of len bytes.
        *
        * @param len the length of the input.
        * @return the space required to accommodate a call to update and doFinal
        * with len bytes of input.
        */
        public int GetOutputSize(
            int length)
        {
            // Note: Can assume IsPartialBlockOkay is true for purposes of this calculation
            return length + bufOff;
        }

        /**
        * process an array of bytes, producing output if necessary.
        *
        * @param in the input byte array.
        * @param inOff the offset at which the input data starts.
        * @param len the number of bytes to be copied out of the input array.
        * @param out the space for any output that might be produced.
        * @param outOff the offset from which the output will be copied.
        * @return the number of output bytes copied to out.
        * @exception DataLengthException if there isn't enough space in out.
        * @exception InvalidOperationException if the cipher isn't initialised.
        */
        public int ProcessBytes(
            byte[] input,
            int inOff,
            int length,
            byte[] output,
            int outOff,
            ProcessStepBase previousStep, out ProcessStepBase currentStep)
        {
            if (length < 1)
            {
                if (length < 0)
                    throw new ArgumentException("Can't have a negative input length!");

                currentStep = previousStep;
                return 0;
            }

            int blockSize = GetBlockSize();
            int outLength = GetUpdateOutputSize(length);

            if (outLength > 0)
            {
                if ((outOff + outLength) > output.Length)
                {
                    throw new DataLengthException("output buffer too short");
                }
            }


            currentStep = previousStep;
            int resultLen = 0;
            int gapLen = buf.Length - bufOff;
            if (length > gapLen)
            {
                Array.Copy(input, inOff, buf, bufOff, gapLen);
                resultLen += cipher.ProcessBlock(buf, 0, output, outOff, currentStep, out currentStep);
                bufOff = 0;
                length -= gapLen;
                inOff += gapLen;
                while (length > buf.Length)
                {
                    resultLen += cipher.ProcessBlock(input, inOff, output, outOff + resultLen, currentStep, out currentStep);
                    length -= blockSize;
                    inOff += blockSize;
                }
            }
            Array.Copy(input, inOff, buf, bufOff, length);
            bufOff += length;
            if (bufOff == buf.Length)
            {
                resultLen += cipher.ProcessBlock(buf, 0, output, outOff + resultLen, currentStep, out currentStep);
                bufOff = 0;
            }
            return resultLen;
        }

        /**
        * Process the last block in the buffer.
        *
        * @param out the array the block currently being held is copied into.
        * @param outOff the offset at which the copying starts.
        * @return the number of output bytes copied to out.
        * @exception DataLengthException if there is insufficient space in out for
        * the output, or the input is not block size aligned and should be.
        * @exception InvalidOperationException if the underlying cipher is not
        * initialised.
        * @exception InvalidCipherTextException if padding is expected and not found.
        * @exception DataLengthException if the input is not block size
        * aligned.
        */
        public int DoFinal(
            byte[] output,
            int outOff,
            ProcessStepBase previousStep, out ProcessStepBase currentStep)
        {
            try
            {
                if (bufOff != 0)
                {
                    if (!cipher.IsPartialBlockOkay)
                    {
                        throw new DataLengthException("data not block size aligned");
                    }

                    if (outOff + bufOff > output.Length)
                    {
                        throw new DataLengthException("output buffer too short for DoFinal()");
                    }

                    // NB: Can't copy directly, or we may write too much output
                    cipher.ProcessBlock(buf, 0, buf, 0, previousStep, out currentStep);
                    Array.Copy(buf, 0, output, outOff, bufOff);
                }
                else
                {
                    currentStep = previousStep;
                }

                return bufOff;
            }
            finally
            {
                Reset();
            }
        }

        /**
        * Reset the buffer and cipher. After resetting the object is in the same
        * state as it was after the last init (if there was one).
        */
        public void Reset()
        {
            Array.Clear(buf, 0, buf.Length);
            bufOff = 0;

            cipher.Reset();
        }
    }
}
